#!/bin/sh

################################################################################
#                                                                              #
# Astren's hardening script for GNU/Linux systems                              #
#                                                                              #
# Script is written in POSIX shell, for better performance use of              #
# minimal shell like dash is recommended                                       #
#                                                                              # 
# My work is dedicated for a lovely guy, who has been my biggest support       #
# in my fight with depression                                                  #
#                                                                              #
################################################################################

# Initialize color shortcuts
R='\033[38;5;1m' # RED
G='\033[38;5;2m' # GREEN
Y='\033[38;5;3m' # YELLOW
B='\033[38;5;4m' # BLUE
M='\033[38;5;5m' # MAGENTA
C='\033[38;5;6m' # CYAN
W='\033[38;5;7m' # WHITE

BL='\033[5m' # BLINK
BO='\033[1m' # BOLD
UN='\033[4m' # UNDERLINE
RE='\033[m'  # RESET

PASS_ROUNDS=1000000

SET_UMASK=1
DNSCRYPT=0
APPARMOR=1
FIREWALL=1
DISABLE_ROOT=1
H_PASS=1
H_LIMITS=1
H_FSTAB=1
H_SSH=1
H_BOOT=1
H_SYSCTL=1


# Clear screen and create temporary directory for easier cleaning
initScript() {
	clear
	TDIR="/tmp/harden"
	[ -f "$TDIR" ] || mkdir "$TDIR"
	trap cleanBackup EXIT
}

cleanBackup() {
	rm -rf "$TDIR" && printf "${C}Cleaning up${RE}\n"
}

# Get current OS name
whatOS() {
	if [ -e /etc/os-release ]; then
		OSNAME=$(awk -F= '/^ID=/ {print $2}' /etc/os-release)
		# Set install command
		case "$OSNAME" in
			arch)
				PKG="pacman -Sy --noconfirm"
			;;
			debian)
				PKG="apt install"
			;;
		esac
	else
		printf "${R}OS NOT DETECTED, ${UN}PACKAGES WON'T BE INSTALLED${RE}\n"
	fi
}

# Set root shell to nologin
disableRoot() {
	printf "${Y}Disabling root account${RE}\n"
	# Change root shell to nologin
	sed 's/\/root:.*$/\/root:\/sbin\/nologin/' /etc/passwd > ${TDIR}/passwd
	sudo cp --no-preserve=ownership ${TDIR}/passwd /etc/.
	[ -r /etc/passwd ] || sudo chmod 644 /etc/passwd
}

setUmask() {
	if [ "$(umask)" -ne 77 ]; then
		printf "${R}Umask value isn't secure.\n${Y}Changing to 077.${RE}\n"
		# Apply value for every shell rc file if present
		for RCFILE in zshrc bashrc ; do
			if [ -f "${HOME}/.${RCFILE}" ]; then
				if [ -z "$(grep -i 'umask\s\+[0-9]\+' ${HOME}/.${RCFILE})" ]; then
					sed '1s/^/umask 77 # Security option\n/' ${HOME}/.${RCFILE} > ${TDIR}/${RCFILE}
					mv ${TDIR}/${RCFILE} ${HOME}/.${RCFILE}
				else
					# Change only the value if umask entry is present
					sed 's/umask\s\+[0-9]\+/umask 77 # Security option/' ${HOME}/.${RCFILE} > ${TDIR}/${RCFILE}
					mv ${TDIR}/${RCFILE} ${HOME}/.${RCFILE}
				fi
			fi
		done
		# Set value for every created user
		sed 's/^UMASK\s\+[0-9]\+/UMASK 077/' /etc/login.defs > ${TDIR}/login.defs
		sudo cp --no-preserve=ownership ${TDIR}/login.defs /etc/login.defs
		# Login.defs must be readable for other users
		sudo chmod 644 /etc/login.defs
	fi
	# Change umask to 22 when using sudo (NEEDED to avoid system breakage)
	[ -f /etc/sudoers.d/chmask ] || {
		[ -z "$(sudo grep '^\s*Defaults\s*umask=0022' /etc/sudoers)" ] && sudo printf "Defaults umask=0022\n" >> ${TDIR}/chmask
		[ -z "$(sudo grep '^\s*Defaults\s*=umask_override' /etc/sudoers)" ] && sudo printf "Defaults umask_override\n" >> ${TDIR}/chmask
		sudo cp --no-preserve=ownership ${TDIR}/chmask /etc/sudoers.d/chmask
		[ -w /etc/sudoers/chmask -o -r /etc/sudoers/chmask ] && sudo chmod 400 /etc/sudoers.d/chmask
	}
	# Set mode for all files in home to 600
	chmod -R go-rwx /home/* 2>/dev/null
}

hardenFstab() {
	printf "${Y}Hardening mount options${RE}\n"
	# Backup current fstab
	cp /etc/fstab ${TDIR}/fstab.bak
	# Add /dev/shm /tmp entries if not present
	[ -z "$(grep '/dev/shm' ${TDIR}/fstab.bak)" ] && printf "tmpfs\t/dev/shm\ttmpfs\trw,size=1G\t0 0\n" >> ${TDIR}/fstab.bak
	[ -z "$(grep '/dev\s' ${TDIR}/fstab.bak)" ] && printf "dev\t/dev\tdevtmpfs\trw,noexec,nosuid,mode=755\t0 0\n" >> ${TDIR}/fstab.bak
	[ -z "$(grep '/tmp' ${TDIR}/fstab.bak)" ] && printf "tmpfs\t/tmp\ttmpfs\trw,size=1G\t0 0\n" >> ${TDIR}/fstab.bak
	[ -z "$(grep 'hidepid' ${TDIR}/fstab.bak)" ] && printf "proc\t/proc\tproc\tnosuid,nodev,noexec,hidepid=2,gid=proc\t0 0" >> ${TDIR}/fstab.bak
	# Add hardened options if not present
	awk '/\/var[[:blank:]]|\/tmp|\/dev\/shm|\/home/ { if(!/noatime/) {$4=$4",noatime"} } 1' ${TDIR}/fstab.bak | \
	awk '/\/var[[:blank:]]|\/tmp|\/dev\/shm/ { if(!/noexec/) {$4=$4",noexec"} } 1' | \
	awk '/\/var[[:blank:]]|\/tmp|\/dev\/shm|\/home/ { if(!/nosuid/) {$4=$4",nosuid"} } 1' | \
	awk '/\/var[[:blank:]]|\/tmp|\/dev\/shm|\/home/ { if(!/nodev/) {$4=$4",nodev"} } 1' > ${TDIR}/fstab
	sudo cp --no-preserve=ownership ${TDIR}/fstab /etc/fstab
	# Fstab must be readable for other users
	[ -r /etc/fstab ] || sudo chmod 644 /etc/fstab
}

hardenSSH() {
	# Copy hardened config
	if [ -d /etc/ssh ]; then
		printf "${Y}Hardening SSH config${RE}\n"
		[ -f "opt/openssh/ssh_config" ] && sudo cp --no-preserve=ownership opt/openssh/ssh_config /etc/ssh/ssh_config
		[ -f "opt/openssh/sshd_config" ] && sudo cp --no-preserve=ownership opt/openssh/sshd_config /etc/ssh/sshd_config
		sudo chmod 600 /etc/ssh/*
	else
		printf "${R}WARRNING <--- ${Y}ssh config directory is not present${RE}"
	fi
}

hardenSysctl() {
	# Copy hardened config and apply it
	printf "${Y}Hardening sysctl config${RE}\n"
	if [ -f /etc/sysctl.conf ]; then
		if [ ! -f /etc/sysctl.d/sysctl-sec.conf ]; then
			sudo cp --no-preserve=ownership opt/sysctl-sec.conf /etc/sysctl.d/.
			[ -r /etc/sysctl.d/sysctl-sec.conf ] || sudo chmod 644 /etc/sysctl.d/sysctl-sec.conf
		fi
		# Apply settings
		sudo sysctl -p /etc/sysctl.d/sysctl-sec.conf >/dev/null
	else
		sudo cp --no-preserve=ownership opt/sysctl-sec.conf /etc/sysctl.conf
		[ -r /etc/sysctl.d/sysctl-sec.conf ] || sudo chmod 644 /etc/sysctl.conf
		# Apply settings
		sudo sysctl -p /etc/sysctl.conf >/dev/null
	fi
}

installApparmor() {
	# Install apparmor package if not present
	printf "${Y}Installing apparmor${RE}\n"
	[ -z "$(pacman -Q apparmor)" ] && sudo $PKG apparmor
	# Check if service is enabled
	printf "${Y}Starting apparmor service${RE}\n"
	[ -f /etc/systemd/system/multi-user.target.wants/apparmor.service ] || sudo systemctl enable --now apparmor
	# Add apparmor boot argument
	printf "${Y}Setting needed boot options${RE}\n"
	awk '/GRUB_CMDLINE_LINUX_DEFAULT/ { if(!/apparmor/) sub(/"$/," lsm=apparmor,lockdown apparmor=1\"") } 1' /etc/default/grub > ${TDIR}/grub
	sudo cp --no-preserve=ownership ${TDIR}/grub /etc/default/grub
	[ -r /etc/default/grub ] || sudo chmod 644 /etc/default/grub
	[ $H_BOOT -eq 0 ] && sudo grub-mkconfig -o "$(find /boot -regex '.*grub.cfg' 2>/dev/null)" 2>/dev/null
}

hardenBoot() {
	# Append hardened mitigation options (https://www.kernel.org/doc/html/v4.14/admin-guide/kernel-parameters.html)
	awk '/GRUB_CMDLINE_LINUX_DEFAULT/ {
			if(!/lsm=\S*lockdown/) sub(/lsm=/,"lsm=lockdown,")
			if(!/lockdown=\S*/) sub(/"$/," lockdown=confidentiality\"")
			if(!/spec_store_bypass_disable=on/) sub(/"$/," spec_store_bypass_disable=on\"")
			if(!/spectre_v2=on/) sub(/"$/," spectre_v2=on\"")
			if(!/l1d_flush=on/) sub(/"$/," l1d_flush=on\"")
			if(!/slub_debug=F/) sub(/"$/," slub_debug=F\"")
			if(!/randomize_kstack_offset=1/) sub(/"$/," randomize_kstack_offset=1\"")
		} 1' /etc/default/grub > ${TDIR}/grub
	sudo cp --no-preserve=ownership ${TDIR}/grub /etc/default/.
	[ -r /etc/default/grub ] || sudo chmod 600 /etc/default/grub
	# Mount boot if not present
	[ -z "$(mount | grep '/boot')" ] && sudo mount /boot
	sudo grub-mkconfig -o "$(find /boot -regex '.*grub.cfg' 2>/dev/null)" 2>/dev/null
}


limitsConf () {
	printf "${Y}Hardening limits.conf${RE}\n"
	cp /etc/security/limits.conf ${TDIR}/.
	# Disable core dumps
	[ -z "$(grep '^[^#].*hard\s*core' ${TDIR}/limits.conf)" ] && printf "*\thard\tcore\t0\n" >> ${TDIR}/limits.conf
	[ -z "$(grep '^[^#].*soft\s*core' ${TDIR}/limits.conf)" ] && printf "*\tsoft\tcore\t0\n" >> ${TDIR}/limits.conf
	# Limit maximum number of processes per user
	[ -z "$(grep '^[^#].*hard\s*nproc' ${TDIR}/limits.conf)" ] && printf "*\thard\tnproc\t3000\n" >> ${TDIR}/limits.conf
	[ -z "$(grep '^[^#].*soft\s*nproc' ${TDIR}/limits.conf)" ] && printf "*\tsoft\tnproc\t2000\n" >> ${TDIR}/limits.conf
	sudo cp --no-preserve=ownership ${TDIR}/limits.conf /etc/security/.
}

hardenPass() {
	# Increase password hashing rounds
	awk '/password/ { if(!/rounds/) {$0=$0" '$PASS_ROUNDS'"} } 1' /etc/pam.d/passwd > ${TDIR}/passwd
	[ -s ${TDIR}/passwd ] && sudo cp --no-preserve=ownership ${TDIR}/passwd /etc/pam.d/.
	sed "s/.*#.*SHA_CRYPT_MIN_ROUNDS.*/SHA_CRYPT_MIN_ROUNDS $PASS_ROUNDS/" /etc/login.defs > ${TDIR}/login.defs
	sudo cp --no-preserve=ownership ${TDIR}/login.defs /etc/.
	[ -r /etc/login.defs ] || sudo chmod 644 /etc/login.defs
}

setupFirewall() {
	IPTABLES="sudo iptables"
	DNS1='9.9.9.9'
	DNS2='1.1.1.1'
	# Put here your main internet interface <-----
	DEV=""
	# Clean previous rules
	$IPTABLES -F
	$IPTABLES -X
	$IPTABLES -t nat -F
	$IPTABLES -A INPUT -i lo -j ACCEPT
	$IPTABLES -A OUTPUT -o lo -j ACCEPT
	# Set base policy to drop all packets
	$IPTABLES -P FORWARD DROP
	$IPTABLES -P INPUT DROP
	$IPTABLES -P OUTPUT DROP
	printf "${Y}Creating default connection filter${RE}\n"
	$IPTABLES -N allow-connect
	$IPTABLES -F allow-connect
	# Allow established/related connections
	$IPTABLES -A allow-connect -m state --state ESTABLISHED,RELATED -j ACCEPT
	# Log invalid packets
	$IPTABLES -A allow-connect -i "$DEV" -m limit -j LOG --log-prefix "Error packet at ${DEV}:" 2>/dev/null
	[ $$ -eq 0 ] || printf "\n${R}!WARNING! -> ${B}please fill DEV value in setupFirewall\n\n${RE}"
	$IPTABLES -A allow-connect -j DROP

	$IPTABLES -N ntp-in
	$IPTABLES -F ntp-in
	$IPTABLES -A ntp-in -p udp --sport 123 -j ACCEPT

	$IPTABLES -N ntp-out
	$IPTABLES -F ntp-out
	$IPTABLES -A ntp-out -p udp --dport 123 -j ACCEPT

	printf "${Y}Creating incoming ssh filter${RE}\n"
	$IPTABLES -N ssh-in
	$IPTABLES -F ssh-in
	# Limit rate of SSH login attempts
	$IPTABLES -A ssh-in -m limit --limit 1/second -p tcp --tcp-flags ALL RST --dport ssh -j ACCEPT
	$IPTABLES -A ssh-in -m limit --limit 1/second -p tcp --tcp-flags ALL FIN --dport ssh -j ACCEPT
	$IPTABLES -A ssh-in -m limit --limit 1/second -p tcp --tcp-flags ALL SYN --dport ssh -j ACCEPT
	$IPTABLES -A ssh-in -m state --state RELATED,ESTABLISHED -p tcp --dport ssh -j ACCEPT
	# Create outgoing chains
	$IPTABLES -N ssh-out
	$IPTABLES -F ssh-out
	$IPTABLES -A ssh-out -p tcp --dport ssh -j ACCEPT
	printf "${Y}Creating dns chain${RE}\n"
	$IPTABLES -N dns-out
	$IPTABLES -F dns-out
	$IPTABLES -A dns-out -p udp -d "$DNS1" --dport domain -j ACCEPT
	printf "${Y}Creating http chain${RE}\n"
	$IPTABLES -N www-out
	$IPTABLES -F www-out
	$IPTABLES -A www-out -p tcp --dport www -j ACCEPT
	$IPTABLES -A www-out -p tcp --dport https -j ACCEPT
	# Catch port-scanners
	printf "${Y}Creating port-scanner trap${RE}\n"
	$IPTABLES -N check-flags
	$IPTABLES -F check-flags
	$IPTABLES -A check-flags -p tcp --tcp-flags ALL FIN,URG,PSH -m limit --limit 5/minute -j LOG --log-level alert --log-prefix "NMAP-XMAS:"
	$IPTABLES -A check-flags -p tcp --tcp-flags ALL FIN,URG,PSH -j DROP
	$IPTABLES -A check-flags -p tcp --tcp-flags ALL ALL -m limit --limit 5/minute -j LOG --log-level 1 --log-prefix "XMAS:"
	$IPTABLES -A check-flags -p tcp --tcp-flags ALL ALL -j DROP
	$IPTABLES -A check-flags -p tcp --tcp-flags ALL SYN,RST,ACK,FIN,URG -m limit --limit 5/minute -j LOG --log-level 1 --log-prefix "XMAS-PSH:"
	$IPTABLES -A check-flags -p tcp --tcp-flags ALL SYN,RST,ACK,FIN,URG -j DROP
	$IPTABLES -A check-flags -p tcp --tcp-flags ALL NONE -m limit --limit 5/minute -j LOG --log-level 1 --log-prefix "NULL_SCAN:"
	$IPTABLES -A check-flags -p tcp --tcp-flags ALL NONE -j DROP
	$IPTABLES -A check-flags -p tcp --tcp-flags SYN,RST SYN,RST -m limit --limit 5/minute -j LOG --log-level 5 --log-prefix "SYN/RST:"
	$IPTABLES -A check-flags -p tcp --tcp-flags SYN,RST SYN,RST -j DROP
	$IPTABLES -A check-flags -p tcp --tcp-flags SYN,FIN SYN,FIN -m limit --limit 5/minute -j LOG --log-level 5 --log-prefix "SYN/FIN:"
	$IPTABLES -A check-flags -p tcp --tcp-flags SYN,FIN SYN,FIN -j DROP
	# Allow outgoing tor connections (https://gitlab.torproject.org/legacy/trac/-/wikis/doc/BlockingNonTorTraffic)
	printf "${Y}Allowing tor connection${RE}\n"
	$IPTABLES -A OUTPUT -m owner --uid-owner tor -j ACCEPT
	[ "$OSNAME" = "debian" ] && $IPTABLES -A OUTPUT -m owner --uid-owner debian-tor -j ACCEPT
	# Apply to main chains
	printf "${M}Applying for INPUT${RE}\n"
	$IPTABLES -A INPUT -m state --state INVALID -j DROP
	$IPTABLES -A INPUT -j check-flags
	$IPTABLES -A INPUT -i lo -j ACCEPT
	$IPTABLES -A INPUT -j ssh-in
	$IPTABLES -A INPUT -j allow-connect
	$IPTABLES -A INPUT -j ntp-in
	printf "${M}Applying for FORWARD${RE}\n"
	$IPTABLES -A FORWARD -m state --state INVALID -j DROP
	$IPTABLES -A FORWARD -j check-flags
	$IPTABLES -A FORWARD -o lo -j ACCEPT
	$IPTABLES -A FORWARD -j ssh-in
	$IPTABLES -A FORWARD -j dns-out
	$IPTABLES -A FORWARD -j www-out
	$IPTABLES -A FORWARD -j allow-connect
	printf "${M}Applying for OUTPUT${RE}\n"
	$IPTABLES -A OUTPUT -m state --state INVALID -j DROP
	$IPTABLES -A OUTPUT -j check-flags
	$IPTABLES -A OUTPUT -o lo -j ACCEPT
	$IPTABLES -A OUTPUT -j dns-out
	$IPTABLES -A OUTPUT -j ssh-out
	$IPTABLES -A OUTPUT -j www-out
	$IPTABLES -A OUTPUT -j ntp-out
	$IPTABLES -A OUTPUT -j allow-connect
	printf "${M}Saving iptables${RE}\n"
	sudo iptables-save -f /etc/iptables/iptables.rules
	# Change nameserver to match firewall rules (dnscrypt uses localhost as nameserver proxy)
	[ $DNSCRYPT -eq 1 ] && \
	[ -z "$(grep 'nameserver\s*127.0.0.1' /etc/resolv.conf)" ] && sed '1s/^/nameserver 127.0.0.1\n/' /etc/resolv.conf > ${TDIR}/resolv.conf || \
	[ -z "$(grep "nameserver\s*[${DNS1}|${DNS2}]" /etc/resolv.conf)" ] && sed "1s/^/nameserver ${DNS1}\nnameserver ${DNS2}\n/" /etc/resolv.conf > ${TDIR}/resolv.conf
	sudo [ -w /etc/resolv.conf ] || sudo chattr -i /etc/resolv.conf
	sudo cp --no-preserve=ownership ${TDIR}/resolv.conf /etc/. 2>/dev/null
	# Set immutable flag for the file, so it won't be overwrited
	[ -r /etc/resolv.conf ] || sudo chmod 644 /etc/resolv.conf
	sudo chattr +i /etc/resolv.conf
}

[ "$(id -u)" -ne 0 ] && printf "${R}DO NOT RUN THIS SCRIPT AS ROOT!${RE}\n" || exit 1
initScript
whatOS
[ $SET_UMASK -eq 1 ] && setUmask
[ $DISABLE_ROOT -eq 1 ] && disableRoot
[ $H_FSTAB -eq 1 ] && hardenFstab
[ $H_SSH -eq 1 ] && hardenSSH
[ $H_SYSCTL -eq 1 ] && hardenSysctl
[ $APPARMOR -eq 1 ] && installApparmor
[ $H_BOOT -eq 1 ] && hardenBoot
[ $FIREWALL -eq 1 ] && setupFirewall
[ $H_LIMITS -eq 1 ] && limitsConf
[ $H_PASS -eq 1 ] && hardenPass
